using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.IO;
using System.Net;

namespace Chanutils
{

    // Some structs to neaten stuff
    public struct Connection
    {
        public string IP;
        public int Port;
    }

    // Events, 'cos we like them.
    delegate void DS_OnRecieveIPList(Connection[] IPs);
    delegate void DS_OnPortTestResult(bool Result);

    // Lots of custom exceptions, to please blanky-dude :)
    //   AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
    class InvalidResponse : Exception
    {
        public InvalidResponse() { }
        public InvalidResponse(string message) : base(message) { }

    }
    class FailedToHandshake : Exception
    {
        public FailedToHandshake() { }
        public FailedToHandshake(string message) : base(message) { }

    }
    class NoIPsInPool : Exception
    {
        public NoIPsInPool() { }
        public NoIPsInPool(string message) : base(message) { }

    }
    class PortTestFailed : Exception
    {
        public PortTestFailed() { }
        public PortTestFailed(string message) : base(message) { }

    }
    class UnknownCommandFromClient : Exception
    {
        public UnknownCommandFromClient() { }
        public UnknownCommandFromClient(string message) : base(message) { }

    }
    class UnknownCommandFromServer : Exception
    {
        public UnknownCommandFromServer() { }
        public UnknownCommandFromServer(string message) : base(message) { }

    }
    class UnknownError : Exception
    {
        public UnknownError() { }
        public UnknownError(string message) : base(message) { }

    }
    class NotConnected : Exception
    {
        public NotConnected() { }
        public NotConnected(string message) : base(message) { }

    }
    
    // And finally, the class! ^^
    class DSClient
    {

        // Vars
        private TcpClient Socket;
        private StreamReader Reader;
        private StreamWriter Writer;
        private int P2PPort;
        private string ClientVersion;
        public string IP;
        public int Port;
        public bool SocketConnected
        {
            get
            {
                if (Socket != null)
                    return Socket.Connected;
                else
                    return false;
            }
        } // This is whether the socket is connected
        private bool pConnected; // Whether Handshake is done. -- Ignore ^^
        private bool pSyncPortTestHelper;
        private bool pSyncPortTestResultTmp;
        public bool Connected // Whether Socket & Handshake is connected.
        {
            get
            {
                if (pConnected && SocketConnected)
                    return true;
                else
                    return false;
            }
        }
        public event DS_OnRecieveIPList OnRecieveIPList;
        public event DS_OnPortTestResult OnPortTestResult;

        // Constructor
        public DSClient(string _IP, int _Port, int _P2Port, string _ClientVersion)
        {

            // Call Connect
            Connect(_IP, _Port, _P2Port, _ClientVersion);

        }

        // Connect
        public void Connect(string _IP, int _Port, int _P2PPort, string _ClientVersion)
        {

            // Set vars
            IP = _IP;
            Port = _Port;
            P2PPort = _P2PPort;
            ClientVersion = _ClientVersion;

            // Connect
            Socket = new TcpClient();
			Socket.Client.BeginConnect(new System.Net.IPEndPoint(System.Net.IPAddress.Parse(IP), Port), new AsyncCallback(ConnectCallBack), new Object());
        }

        // Callback for Async Connect
        private void ConnectCallBack(IAsyncResult Result)
        {

            // Not sure whether we should be throwing exceptions here or what. Damn 
            // weird functions. >.>

            // Set up streams if it worked & VERSION etc.
            if (Socket.Connected) // Just to be sure >.>
            {

                // Setup streams!
                Writer = new StreamWriter(Socket.GetStream());
                Reader = new StreamReader(Socket.GetStream());

                // VERSION -- This doesn't need to use our own WriteLine() as it's safe; it will always be connected to socket OK.
                Writer.WriteLine("VERSION " + ClientVersion);
                Writer.WriteLine("PORT " + P2PPort);
                Writer.WriteLine("CONNECTED");
                Writer.Flush();

            }
            else
                throw new SocketException(); // Guess this should eventually be replaced with a custom exception.

        }

        // Write Line to socket
        private void WriteLine(string message)
        {

            // Connected?
            if (Connected)
            {

                // ^^
                Writer.WriteLine(message);
                Writer.Flush();

            }
            else
                throw new NotConnected("Tried to send a message when not connected to Directory Server.");

        }

        // Poll
        public void Poll()
        {

            // Data?
            if (Socket.GetStream().DataAvailable)
            {

                // Get it
                string data = Reader.ReadLine();
                string[] Split = data.Split(" ".ToCharArray(), 2);

                // Parse it
                switch (Split[0])
                {

                    // CONNECTED
                    case "CONNECTED":

                        // ..
                        pConnected = true;

                        break;

                    // IPS - Response to REQUEST
                    case "IPS":

                        // Check there were replies
                        if (Split.Length != 2)
                            return; // We'll just ignore.. User does not need to know.

                        // Pass
                        Response_IPS(Split[1]);

                        break;

                    // ERROR - Response to ANYTHING.
                    case "ERROR":

                        // Check there was something there
                        if (Split.Length != 2)
                            return; 

                        // Pass
                        Response_ERROR(Split[1]);

                        break;
                        
                   	// PORTTESTSUCCESS - FAIRLY SELF-EXPLANATORY. I LIEK CAPS
                   	case "PORTTESTSUCCESS":
                   	
                   		// Check we don't have somebody else's data
                   		if (Split.Length != 1)
                   			// Ohsh-
                   			return;
                   		
                   		// ^^;
                   		Response_PORTTESTSUCCESS();
                   		
                   		break;
                   
                    // .. 
                    default:

                        // ..
                        throw new UnknownCommandFromServer(Split[0]);

                }

            }

        }

        // Response to IPS
        private void Response_IPS(string Params)
        {

            // Split list
            string[] Split = Params.Split(" ".ToCharArray());

            // Make vars
            Connection[] IPs = new Connection[Split.Length];
            string[] Temp;

            // For each IP, split the port + IP
            for (int i = 0; i < Split.Length; i++)
            {

                // Split!
                Temp = Split[i].Split(":".ToCharArray());

                // There was a : Riiight? Error if not, not like we can easily ignore.
                if (Temp.Length != 2)
                    throw new Exception("Invalid response from server; irrecoverable.");

                // ..
                IPs[i].IP = Split[0];
                IPs[i].Port = int.Parse(Split[1]);

            }

            // Throw Event
            if (OnRecieveIPList != null)
                OnRecieveIPList(IPs);

        }

        // Response to ERROR
        private void Response_ERROR(string Params)
        {

            // Split! ^^
            string[] Split = Params.Split(" ".ToCharArray(), 2);

            // What error code?
            switch (Split[0])
            {

                // Couldn't handshake.
                case "00":
                    throw new FailedToHandshake(Split[1]);

                // No IPs
                case "01":
                    throw new NoIPsInPool(Split[1]);

                // Port Test Failed
                case "02":
                	if (OnPortTestResult != null)
                	{
                		// Excellent.  We have a hook to fux with
                		// ... even though the result is a negative :(
                		OnPortTestResult(false);
                	}
                	else
                	{
                		// No hook, so we'll throw an exception!  D=
	                    throw new PortTestFailed(Split[1]);
                	}
                	
                	break;

                // Unknown Command
                case "03":
                    throw new UnknownCommandFromClient(Split[1]);

                // Default
                default:
                    throw new UnknownError(Params);

            }

        }
        
        private void Response_PORTTESTSUCCESS()
        {
        	
        	// Are we hooked, at the very least?
        	if (OnPortTestResult != null)
        		// Snbbrj;ldkfjwe""
        		OnPortTestResult(true);
        	else
        		// .. WHY.  However, in keeping with Mikey's coding "style", we'll swallow it.
        		return;
        	
        }

        // Request IPs
        public void RequestIPs(int Count)
        {

            // Send request
            WriteLine("REQUEST " + Count);

        }

        // Port Test
        public void PortTest()
        {

            // Do port-test, baby!
            WriteLine("PORTTEST " + P2PPort.ToString());

        }
        
        // Synchronous port test.  Because I only implemented the freaky async thing to keep in with the feel of this code
        public bool SynchronousPortTest()
        {
        	
        	// Keep a copy of the old hooks (if any) here...
        	DS_OnPortTestResult oldOnPortTestResult;
        	
        	// Is it not null atm? :/
        	if (OnPortTestResult != null)
        		oldOnPortTestResult = OnPortTestResult;
			else
				// GOD ALMIGHTY, THE COMPILER IS ACTUALLY WORSE THAN JAVA o.o"
        		oldOnPortTestResult = null;
        	
        	// Ok. Now put our shizzle in. ^^
        	pSyncPortTestHelper = false;
        	OnPortTestResult = new DS_OnPortTestResult(SyncPortTestHelper);
        	
        	// Call the port testing thang.
        	PortTest();
        	
        	// Wait for a result...
        	do
        	{
        		System.Threading.Thread.Sleep(5);
        	}
        	while (!pSyncPortTestHelper);
        	
        	// Yay. Now restore the user's other stuffs...
        	if (oldOnPortTestResult != null)
	        	OnPortTestResult = oldOnPortTestResult;
        	
        	// Awesome!
        	return pSyncPortTestResultTmp;
        }
        
        private void SyncPortTestHelper(bool bResult)
        {
        	
        	// Copy over result...
        	pSyncPortTestResultTmp = bResult;
        	// ... aaaand signal we're done. ^^
        	pSyncPortTestHelper = true;
        	
        }

    }
}
